1 module hip.util.path;
2 import hip.util.string;
3 import hip.util.system;
4 //Node required for buildFolderTree
5 public import hip.util.data_structures: Node;
6 
7 version(Windows) 
8     enum defaultCaseSensitivity = false;
9 else version(Darwin) 
10     enum defaultCaseSensitivity = false;
11 else// version(Posix) 
12     enum defaultCaseSensitivity = true;
13 
14 version(Windows)
15 {
16     enum pathSeparator = '\\';
17     enum otherSeparator = '/';
18 }
19 else
20 {
21     enum pathSeparator = '/';
22     enum otherSeparator = '\\';
23 }
24 
25 string[] pathSplitter(string path) @safe pure nothrow
26 {
27     string[] ret;
28     foreach(p; pathSplitterRange(path))
29         ret~= p;
30     return ret;
31 }
32 
33 auto pathSplitterRange(string path) pure @safe nothrow @nogc
34 {
35     struct PathRange
36     {
37         string path;
38         size_t indexRight = 0;
39 
40         bool empty() @safe pure nothrow @nogc {return indexRight >= path.length;}
41         string front() @safe pure nothrow @nogc
42         {
43             size_t i = indexRight;
44             while(i < path.length && path[i] != '\\' && path[i] != '/')
45                 i++;
46             indexRight = i;
47             return path[0..indexRight];
48         }
49         void popFront() @safe pure nothrow @nogc
50         {
51             if(indexRight+1 < path.length)
52             {
53                 path = path[indexRight+1..$];
54                 indexRight = 0;
55             }
56             else
57                 indexRight+= 1; //Guarantees empty
58         }
59     }
60 
61     return PathRange(path);
62 }
63 
64 bool isRootOf(string theRoot, string ofWhat) pure nothrow @nogc
65 {
66     auto pathA = pathSplitterRange(theRoot);
67     auto pathB = pathSplitterRange(ofWhat);
68 
69     for(; !pathA.empty && !pathB.empty; pathA.popFront, pathB.popFront)
70     {
71         string compA = pathA.front;
72         string compB = pathB.front;
73         if(compA != compB)
74             return false;
75     }
76     return true;
77 }
78 
79 
80 
81 string relativePath(bool caseSensitive = defaultCaseSensitivity)(string filePath, string base) pure nothrow @safe
82 {
83     int commonIndex = 0;
84     bool isEqual = true;
85     for(int i = 0; i < base.length; i++)
86     {
87         if(i == filePath.length || (caseSensitive ? base[i] != filePath[i] : base[i].toLowerCase != filePath[i].toLowerCase))
88         {
89             isEqual = false;
90             break;
91         }
92         else if(base[i] == pathSeparator)
93             commonIndex = cast(int)i;
94     }
95     if(isEqual)
96     {
97         if(filePath.length == base.length)
98             return ".";
99         else //If the base string is a subset, return part after base.
100             return filePath[base.length + (filePath[base.length] == pathSeparator ? 1 : 0)..$];
101     } 
102     else if(commonIndex == 0)
103         return filePath;
104 
105     string ret;
106     for(uint i = commonIndex; i < base.length; i++)
107     {
108         if(base[i] == pathSeparator)
109             ret~= ".."~pathSeparator;
110     }
111     ret~= filePath[commonIndex] == pathSeparator ? filePath[commonIndex+1..$] : filePath[commonIndex..$];
112     return ret;
113 }
114 
115 bool isAbsolutePath(string fPath) pure nothrow @nogc @safe
116 {
117     if(fPath == null)
118         return false;
119     version(Posix)
120         if(fPath[0] != '/')
121             return false;
122     version(Windows)
123     {
124         if(fPath.length < 3)
125             return false;
126         if(!(fPath[0].isUpperCase && fPath[1] == ':' && fPath[2] == '\\'))
127             return false;
128     }
129     for(size_t i = 0; i < fPath.length; i++)
130         if(i + 2 < fPath.length && fPath[i] == '.' && fPath[i+1] == '.' && fPath[i+2] == pathSeparator)
131             return false;
132     return true;
133 }
134 
135 string absolutePath(string thePath, string currPath)
136 {
137     if(isAbsolutePath(thePath)) return thePath;
138     return joinPath(currPath, thePath);
139 }
140 
141 
142 
143 char determineSeparator(const string filePath) pure nothrow @nogc @safe
144 {
145     size_t i = 0;
146     while(i < filePath.length && filePath[i] != '/' && filePath[i] != '\\')
147         i++;
148     return i < filePath.length ? filePath[i] : '\0';
149 }
150 
151 ///Will get the directory name until a trailing separator or return 
152 string dirName(string filePath) pure nothrow @nogc @safe
153 {
154     char sep = determineSeparator(filePath);
155     if(sep == '\0')
156         return ".";
157     int last = filePath.lastIndexOf(sep);
158     if(last == -1)
159         return ".";
160     return filePath[0..last];
161 }
162 
163 
164 string filename(string filePath) @safe pure nothrow @nogc
165 {
166     char sep = determineSeparator(filePath);
167     if(sep == '\0')
168         return filePath;
169     int last = filePath.lastIndexOf(sep);
170     if(last == -1)
171         return filePath;
172     return filePath[last+1..$];
173 }
174 
175 alias baseName = filename;
176 
177 ref string filename(return ref string filePath, string newFileName) @safe pure nothrow
178 {
179     return filePath = replaceFileName(filePath, newFileName);
180 }
181 
182 string filenameNoExt(string filePath) @safe pure nothrow @nogc
183 {
184     string f = filePath.filename;
185     if(f == "")
186         return "";
187     int last = f.lastIndexOf(".");
188     if(last == -1)
189         return f;
190     return f[0..last];
191 }
192 
193 string replaceFileName(string filePath, string newFileName) @safe pure nothrow
194 {
195     char sep = determineSeparator(filePath);
196     string[] p = pathSplitter(filePath);
197     p[$-1] = newFileName;
198     return ((p[0] == "" && sep == '/') ? "/" : "") ~ joinPath(sep, p);
199 }
200 
201 string normalizePath(string path)
202 {
203     string[16] normalized;
204     size_t pathsLength;
205     foreach(p; pathSplitterRange(path))
206     {
207         if(p == ".")
208             continue;
209         else if(p == "..")
210         {
211             if(pathsLength > 0)
212                 pathsLength--;
213             else
214                 normalized[pathsLength++]= p;
215         }
216         else
217             normalized[pathsLength++]= p;
218 
219     }
220     return normalized[0..pathsLength].joinPath;
221 }
222 
223 
224 
225 /**
226 *   Extension getter
227 ```d
228 string myFile = "test.png";
229 writeln(myFile.extension); //png
230 ```
231 */
232 string extension(string pathOrFilename) pure nothrow @nogc @safe
233 {
234     auto ind = pathOrFilename.lastIndexOf(".");
235     if(ind == -1)
236         return "";
237     return pathOrFilename[cast(uint)ind+1..$];
238 }
239 
240 /**
241 *   Extension setter.
242 *   Usage:
243 ```d
244    string test = "test.png"
245    test.extension = "txt";
246    writeln(test); //test.txt
247 ```
248 */
249 ref string extension(return ref string pathOrFilename, string newExt)
250 {
251     auto ind = pathOrFilename.lastIndexOf(".");    
252     if(ind != -1 && ind != pathOrFilename.length)
253     {
254         if(newExt.length == 0)
255             pathOrFilename = pathOrFilename[0..ind];
256         else if(newExt[0] != '.')
257             pathOrFilename = pathOrFilename[0..ind+1]~newExt;
258         else
259             pathOrFilename = pathOrFilename[0..ind+1]~newExt[1..$];
260     }
261     return pathOrFilename;
262 }
263 
264 string extension(string pathOrFilename, string newExt)
265 {
266     pathOrFilename = pathOrFilename.extension(newExt);
267     return pathOrFilename;
268 }
269 
270 string joinPath(char separator, scope const string[] paths ...) @safe pure nothrow
271 {
272     if(paths.length == 1)
273         return paths[0];
274 
275     PathString output;
276     for(int i = 0; i < paths.length; i++)
277     {
278         string next = i+1 < paths.length ? paths[i+1] : "";
279         if(paths[i] != "")
280         {
281             output~=paths[i];
282             if(next != "" && next[0] != separator  &&
283             paths[i][$-1] != separator)
284                 output~=separator;
285         }
286         else
287         {
288             if(next != "" && next[0] != separator)
289                 output~= separator;
290         }
291     }
292     return output.toString.dup;
293 }
294 
295 string joinPath(scope const string[] paths ...) @safe pure nothrow
296 {
297     char sep;
298     foreach(p; paths)
299     {
300         sep = determineSeparator(p);
301         if(sep != '\0')
302             break;
303     }
304     if(sep == '\0')
305         sep = pathSeparator;
306     return joinPath(sep, paths);
307 }
308 
309 
310 public Node!string buildFolderTree(string[] filesList)
311 {
312     alias DirNode = Node!string;
313     DirNode root = new DirNode(filesList[0]);
314 
315     scope DirNode[] dirStack = [root];
316     
317     for(size_t i = 1; i < filesList.length; i++)
318     {
319         int currStack = 0;
320         foreach(pathPart; pathSplitterRange(filesList[i]))
321         {
322             if(pathPart.extension != "") //It is a leaf if it has an extension
323             {
324                 dirStack[$-1].addChild(pathPart);
325             }
326             else if(currStack >= dirStack.length) //If we have more parts than the stack has children, add to the stack
327             {
328                 //Add child to the last
329                 dirStack~= dirStack[$-1].addChild(pathPart);
330                 currStack++;
331             }
332             else if(dirStack[currStack].data != pathPart) //If both they are the same, check for the next part
333             {
334                 dirStack = dirStack[0..$-1];
335                 currStack--;
336             }
337             else if(dirStack[currStack].data == pathPart) //If both they are the same, check for the next part
338                 currStack++;
339         }
340     }
341     return root;
342 }
343 string buildPath(Node!string node)
344 {
345     string ret = node.data;
346     while(node.parent !is null)
347     {
348         node = node.parent;
349         if(node)
350         {
351             ret = node.data~"/"~ret;
352         }
353     }
354     return ret;
355 }
356 
357 
358 ///Copied from dmd.
359 unittest
360 {
361     assert(baseName("a/b/test.txt") == "test.txt");
362     assert(relativePath("foo", "") == "foo");
363     assert(filenameNoExt("helloWorld.zip") == "helloWorld");
364     assert("/hello/test/again".isRootOf("/hello/test/again/something/is/here.txt"));
365 
366     version (Posix)
367     {
368         assert(filename("/something/here/yet.txt"), "yet.txt");
369         assert(filenameNoExt("/something/here/yet.txt"), "yet");
370 
371         assert(relativePath("foo", "/bar") == "foo");
372         assert(relativePath("/foo/bar", "/foo/bar") == ".");
373         assert(relativePath("/foo/bar", "/foo/baz") == "../bar");
374         assert(relativePath("/foo/bar/baz", "/foo/woo/wee") == "../../bar/baz");
375         assert(relativePath("/foo/bar/baz", "/foo/bar") == "baz");
376     }
377     version (Windows)
378     {
379         assert(filename(`c:\something\here\yet.txt`), "yet.txt");
380         assert(filenameNoExt(`c:\something\here\yet.txt`) == "yet");
381 
382         assert(relativePath("foo", `c:\bar`) == "foo");
383         assert(relativePath(`c:\foo\bar`, `c:\foo\bar`) == ".");
384         assert(relativePath(`c:\foo\bar`, `c:\foo\baz`) == `..\bar`);
385         assert(relativePath(`c:\foo\bar\baz`, `c:\foo\woo\wee`) == `..\..\bar\baz`);
386         assert(relativePath(`c:\foo\bar\baz`, `c:\foo\bar`) == "baz");
387         assert(relativePath(`c:\foo\bar`, `d:\foo`) == `c:\foo\bar`);
388     }
389 }